package com.nativelibs4java.buswing;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.table.TableModel;
import javax.swing.tree.TreePath;

/**
 * A TableModel to MiningModel adapter that should be subclassed to fit actual requirements of real-world components (for instance, see DefaultMiningModel)
 * @author ochafik
 */
public abstract class AbstractMiningModel implements MiningModel {
    private final TableModel model;
    private final Set<DefaultMiningColumn> groupColumnsSet = new HashSet<DefaultMiningColumn>();
    private List<DefaultMiningColumn> groupColumns = new ArrayList<DefaultMiningColumn>();
    private DefaultMiningColumn iconColumn;
    private DefaultMiningColumn valueColumn;
    private final PersistentItems<DefaultMiningRow> persistentRows = new PersistentItems<DefaultMiningRow>() {

        protected DefaultMiningRow createItem(long persistentId, int itemIndex) {
            return new DefaultMiningRow(persistentId, itemIndex);
        }
    };

    private final PersistentItems<DefaultMiningColumn> persistentColumns = new PersistentItems<DefaultMiningColumn>() {

        protected DefaultMiningColumn createItem(long persistentId, int itemIndex) {
            return new DefaultMiningColumn(persistentId, itemIndex);
        }
    };

    private final List<TreeModelListener> treeListeners = new ArrayList<TreeModelListener>();
    private final Map<TreeModelListener, TreeModelListener> convertedTreeListeners = new HashMap<TreeModelListener, TreeModelListener>();
    private final List<TableModelListener> tableListeners = new ArrayList<TableModelListener>();
    private final Map<TableModelListener, TableModelListener> convertedTableListeners = new HashMap<TableModelListener, TableModelListener>();

    private final Map<DefaultMiningColumnValues, DefaultMiningRow> aggregatedRows = new HashMap<DefaultMiningColumnValues, DefaultMiningRow>();
    private final Map<DefaultMiningRow, DefaultMiningNode> nodeByRow = new HashMap<DefaultMiningRow, DefaultMiningNode>();
    private final Map<DefaultMiningColumn, List<DefaultMiningNode>> internalNodesByColumn = new HashMap<DefaultMiningColumn, List<DefaultMiningNode>>();
    private final Map<DefaultMiningNode, List<DefaultMiningNode>> nodesChildren = new HashMap<DefaultMiningNode, List<DefaultMiningNode>>();
    private final Map<DefaultMiningNode, List<DefaultMiningRow>> nodesFilteredRows = new HashMap<DefaultMiningNode, List<DefaultMiningRow>>();
    private final List<DefaultMiningColumn> visibleColumns = new ArrayList<DefaultMiningColumn>();

    public AbstractMiningModel(TableModel model) {
        this.model = model;
        rebuild();
        model.addTableModelListener(new TableModelListener() {

            @Override
            public void tableChanged(TableModelEvent e) {
                int count = e.getLastRow() - e.getFirstRow() + 1;
                switch (e.getType()) {
                    case TableModelEvent.INSERT:
                        rowInserted(e.getFirstRow(), count);
                        break;
                    case TableModelEvent.DELETE:
                        rowRemoved(e.getFirstRow(), count);
                        break;
                    case TableModelEvent.UPDATE:
                        rowUpdated(e.getFirstRow(), count);
                }
            }
        });

    }

    List<MiningModelListener> miningModelListeners = new ArrayList<MiningModelListener>();

    public void addMiningModelListener(MiningModelListener l) {
        miningModelListeners.add(l);
    }

    public void removeMiningModelListener(MiningModelListener l) {
        miningModelListeners.remove(l);
    }

    public DefaultMiningColumn getColumn(String name) {
        for (int i = 0, n = model.getColumnCount(); i < n; i++) {
            DefaultMiningColumn column = getUnderlyingColumn(i);
            if (name.equals(column.getName()))
                return column;
        }
        return null;
    }

    public void setGroupColumns(List<MiningColumn> groupColumns) {
        Set<DefaultMiningColumn> colSet = new LinkedHashSet<DefaultMiningColumn>();
        for (MiningColumn col : groupColumns) {
            if (col instanceof DefaultMiningColumn)
                colSet.add((DefaultMiningColumn)col);
            else
                throw new IllegalArgumentException("Not a DefaultMiningColumn : " + col);
        }
        this.groupColumns = new ArrayList<DefaultMiningColumn>(colSet);
        groupColumnsSet.clear();
        groupColumnsSet.addAll(this.groupColumns);

        for (DefaultMiningColumn column : this.groupColumns)
            column.setGroupIndex(-1);

        int groupIndex = 0;
        for (DefaultMiningColumn column : this.groupColumns) {
            column.setGroupIndex(groupIndex++);
        }

        rebuild();
        fireGroupColumnsChanged();
    }
    void rebuild() {
        visibleColumns.clear();
        for (int columnIndex = 0, columnCount = model.getColumnCount(); columnIndex < columnCount; columnIndex++) {
            DefaultMiningColumn column = persistentColumns.getItem(columnIndex, true);
            if (groupColumnsSet.contains(column) || 
                (!groupColumns.isEmpty() && column == iconColumn)) {
                column.setVisibleIndex(-1);
            } else {
                visibleColumns.add(column);
            }
        }
        Collections.sort(visibleColumns, new Comparator<DefaultMiningColumn>() {

            @Override
            public int compare(DefaultMiningColumn o1, DefaultMiningColumn o2) {
                return o1.getUnderlyingIndex() - o2.getUnderlyingIndex();
            }

        });
        for (int i = 0, n = visibleColumns.size(); i < n; i++)
            visibleColumns.get(i).setVisibleIndex(i);


        clearCaches();

        fireTableChanged();
        fireStructureChanged(root);
    }

    protected void fireGroupColumnsChanged() {
        if (!miningModelListeners.isEmpty()) {
            MiningModelEvent e = new MiningModelEvent(this, MiningModelEvent.GROUP_COLUMNS_CHANGED);
            for (MiningModelListener l : miningModelListeners) {
                l.groupColumnsChanged(e);
            }
        }
    }
    protected void fireStructureChanged(DefaultMiningNode path) {
        if (!treeListeners.isEmpty()) {
            TreeModelEvent e = new TreeModelEvent(this, path.getPath());
            for (TreeModelListener l : treeListeners) {
                l.treeStructureChanged(e);
            }
        }
    }
    protected void fireTableChanged() {
        if (!tableListeners.isEmpty()) {
            TableModelEvent e = new TableModelEvent(this);//, path.getPath());
            for (TableModelListener l : tableListeners) {
                l.tableChanged(e);
            }
        }
    }


    protected void clearCaches() {
        aggregatedRows.clear();
        nodeByRow.clear();
        internalNodesByColumn.clear();
        nodesChildren.clear();
        nodesFilteredRows.clear();
    }

    private DefaultMiningColumn getUnderlyingColumn(int index) {
        return persistentColumns.getItem(index, true);
    }
    public DefaultMiningRow getRow(int index) {
        return persistentRows.getItem(index, true);
    }
    public void addGroupColumn(MiningColumn column) {
        addGroupColumn(getGroupColumns().size(), column);
    }

    public void addGroupColumn(int index, MiningColumn column) {
        List<MiningColumn> columns = new ArrayList<MiningColumn>(getGroupColumns());
        columns.add(index, column);
        setGroupColumns(columns);
    }

    @Override
    public List<MiningColumn> getGroupColumns() {
        return (List<MiningColumn>)(List)Collections.unmodifiableList(groupColumns);
    }


    int getUnderlyingColumnIndex(int columnIndex) {
        DefaultMiningColumn column = columnIndex < visibleColumns.size() ? visibleColumns.get(columnIndex) : null;
        if (column == null)
            return -1;
        return column.getUnderlyingIndex();
        /*if (groupColumns.isEmpty()) {
            return columnIndex;
        }

        int actual = 0;
        for (int i = 0, n = model.getColumnCount(); i < n; i++) {
            if (groupColumnsSet.contains(i)) {
                continue;
            }

            if (actual == columnIndex) {
                return i;
            }

            actual++;
        }
        return -1;*/
    }
    
    @Override
    public void addTableModelListener(final TableModelListener l) {
        TableModelListener c = new TableModelListener() {

            @Override
            public void tableChanged(TableModelEvent e) {
                l.tableChanged(new TableModelEvent(AbstractMiningModel.this));
            }
        };
        convertedTableListeners.put(l, c);
        tableListeners.add(c);
    }

    @Override
    public void addTreeModelListener(final TreeModelListener l) {
        TreeModelListener c = new TreeModelListener() {

            void allChanged() {
                l.treeStructureChanged(new TreeModelEvent(AbstractMiningModel.this, new Object[]{root}));
            }

            @Override
            public void treeNodesChanged(TreeModelEvent e) {
                allChanged();
            }

            @Override
            public void treeNodesInserted(TreeModelEvent e) {
                allChanged();
            }

            @Override
            public void treeNodesRemoved(TreeModelEvent e) {
                allChanged();
            }

            @Override
            public void treeStructureChanged(TreeModelEvent e) {
                allChanged();
            }
        };
        convertedTreeListeners.put(l, c);
        treeListeners.add(c);
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        return model.getColumnClass(getUnderlyingColumnIndex(columnIndex));
    }

    @Override
    public int getColumnCount() {
        return visibleColumns.size();
    }

    @Override
    public String getColumnName(int columnIndex) {
        return model.getColumnName(getUnderlyingColumnIndex(columnIndex));
    }

    @Override
    public int getRowCount() {
        return model.getRowCount();
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        return model.getValueAt(rowIndex, getUnderlyingColumnIndex(columnIndex));
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return model.isCellEditable(rowIndex, getUnderlyingColumnIndex(columnIndex));
    }

    @Override
    public void removeTableModelListener(TableModelListener l) {
        TableModelListener c = convertedTableListeners.remove(l);
        if (c != null) {
            tableListeners.remove(c);
        }
    }

    @Override
    public void removeTreeModelListener(TreeModelListener l) {
        TreeModelListener c = convertedTreeListeners.remove(l);
        if (c != null) {
            treeListeners.remove(c);
        }
    }

    public void setIconColumn(DefaultMiningColumn iconColumn) {
        this.iconColumn = iconColumn;
    }

    public DefaultMiningColumn getIconColumn() {
        return iconColumn;
    }

    class DefaultMiningColumn extends PersistentItems.PersistentItem implements MiningColumn {

        public DefaultMiningColumn(long persistentId, int columnIndex) {
            super(persistentId, columnIndex);
        }

        private int visibleIndex = -1;
        @Override
        public int getVisibleIndex() {
            return visibleIndex;
        }

        void setVisibleIndex(int visibleIndex) {
            this.visibleIndex = visibleIndex;
        }

        @Override
        public String getName() {
            return model.getColumnName(getUnderlyingIndex());
        }
        @Override
        public String toString() {
            return getName();
        }

        private int groupIndex = -1;
        public int getGroupIndex() {
            return groupIndex;
        }

        void setGroupIndex(int groupIndex) {
            this.groupIndex = groupIndex;
        }

    }

    class DefaultMiningRow extends PersistentItems.PersistentItem implements MiningRow {

        public DefaultMiningRow(long persistentId, int columnIndex) {
            super(persistentId, columnIndex);
        }

        @Override
        public String toString() {
            DefaultMiningColumn valueColumn = getValueColumn();
            return valueColumn != null && valueColumn.getUnderlyingIndex() < model.getColumnCount()
                ? String.valueOf(model.getValueAt(getUnderlyingIndex(), valueColumn.getUnderlyingIndex())) :
                    "Row " + getUnderlyingIndex();
        }
    }


    public void setValueColumn(DefaultMiningColumn column) {
        this.valueColumn = column;
    }

    @Override
    public List<MiningColumn> getVisibleColumns() {
        return (List<MiningColumn>)(List)Collections.unmodifiableList(visibleColumns);
    }

    public DefaultMiningColumn getValueColumn() {
        return valueColumn != null ? valueColumn :
            visibleColumns.isEmpty() ? null :
                visibleColumns.get(0);
    }

    void buildNodes(DefaultMiningNode node, int levels) {
        if (levels <= 0)
            return;
        levels--;
        for (DefaultMiningNode child : getNodeChildren(node))
            buildNodes(child, levels);
    }
    @Override
    public List<MiningNode> getGroupNodes(MiningColumn column) {
        List<DefaultMiningNode> ret = new ArrayList<DefaultMiningNode>();
        if (column != null && column.getGroupIndex() >= 0) {
            List<DefaultMiningNode> nodes = internalNodesByColumn.get(column);
            if (nodes == null) {
                buildNodes(getRoot(), column.getGroupIndex() + 1);
                nodes = internalNodesByColumn.get(column);
            }

            if (nodes != null) {
                for (DefaultMiningNode node : nodes) {
                    ret.add(node);
                }
            }
        }
        return (List<MiningNode>)(List)ret;
    }

    class DefaultColumnValue implements MiningColumnValue {

        DefaultMiningColumn column;
        Object value;

        DefaultColumnValue(DefaultMiningColumn column, Object value) {
            this.column = column;
            this.value = value;
        }

        @Override
        public DefaultMiningColumn getColumn() {
            return column;
        }

        @Override
        public Object getValue() {
            return value;
        }

        @Override
        public String toString() {
            return value == null ? "<no " + column.getName() + ">" : value.toString();
        }

        @Override
        public int hashCode() {
            int hc = column.hashCode();
            if (value != null) {
                hc ^= value.hashCode();
            }
            return hc;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof DefaultColumnValue)) {
                return false;
            }

            DefaultColumnValue c = (DefaultColumnValue) obj;
            if (!c.column.equals(column)) {
                return false;
            }
            return equal(c.value, value);
        }

        public boolean accept(DefaultMiningRow row) {
            Object value = model.getValueAt(row.getUnderlyingIndex(), column.getUnderlyingIndex());
            return equal(value, this.value);
        }
    }

    static boolean equal(Object a, Object b) {
        if ((a == null) != (b == null)) {
            return false;
        }
        return a == null || a.equals(b);
    }

    class DefaultMiningColumnValues implements MiningColumnValues {

        final DefaultColumnValue[] columnValues;

        public DefaultMiningColumnValues() {
            this(new DefaultColumnValue[0]);
        }

        public DefaultMiningColumnValues(DefaultColumnValue[] columnValues) {
            this.columnValues = columnValues;
        }

        @Override
        public DefaultColumnValue[] getColumnValues() {
            return columnValues.clone();
        }

        @Override
        public int hashCode() {
            int hc = 0;
            for (DefaultColumnValue columnValue : columnValues) {
                hc ^= columnValue.hashCode();
            }
            return hc;
        }

        @Override
        public DefaultColumnValue getLast() {
            return columnValues.length == 0 ? null : columnValues[columnValues.length - 1];
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof DefaultMiningColumnValues)) {
                return false;
            }
            DefaultMiningColumnValues cv = (DefaultMiningColumnValues) obj;
            if (cv.columnValues.length != columnValues.length) {
                return false;
            }
            for (int i = 0, n = columnValues.length; i < n; i++) {
                if (!equal(columnValues[i], cv.columnValues[i])) {
                    return false;
                }
            }
            return true;
        }

        DefaultMiningColumnValues derive(DefaultMiningColumn column, Object value) {
            DefaultColumnValue[] newValues = new DefaultColumnValue[columnValues.length + 1];
            System.arraycopy(columnValues, 0, newValues, 0, columnValues.length);
            newValues[columnValues.length] = new DefaultColumnValue(column, value);
            return new DefaultMiningColumnValues(newValues);
        }
    }

    static class DefaultMiningNode implements MiningNode {

        final DefaultMiningColumnValues columnValues;
        final DefaultMiningRow row;
        final DefaultMiningNode parent;

        public DefaultMiningNode(DefaultMiningNode parent, DefaultMiningColumnValues columnValues, DefaultMiningRow row) {
            this.parent = parent;
            this.columnValues = columnValues;
            this.row = row;
        }

        @Override
        public String toString() {
            if (row == null) {
                DefaultColumnValue cv = getColumnValues().getLast();
                return cv == null ? null : cv.toString();
            }
            return row.toString();
        }

        public TreePath getPath() {
            Object[] path = new Object[columnValues.columnValues.length + 1];
            int i = path.length;
            DefaultMiningNode n = this;
            do {
                i--;
                path[i] = n;
                n = n.parent;
            } while (n != null && i >= 0);
            //System.arraycopy(columnValues.columnValues, 0, path, 1, columnValues.columnValues.length);
            //path[0] = getRoot();
            return new TreePath(path);
        }

        @Override
        public int hashCode() {
            if (row != null) {
                return row.hashCode();
            } else {
                return columnValues.hashCode();
            }
        }

        public DefaultMiningRow getRow() {
            return row;
        }

        public DefaultMiningColumnValues getColumnValues() {
            return columnValues;
        }

        public DefaultMiningNode getParent() {
            return parent;
        }
    }

    /**
     * Override this to create node summaries (values for internal nodes in the tree)
     * @param columnValues
     * @return
     */
    protected DefaultMiningRow computeAggregatedRow(DefaultMiningColumnValues columnValues, List<DefaultMiningRow> rows) {
        return null;
    }

    DefaultMiningRow getAggregatedRow(DefaultMiningColumnValues columnValues, List<DefaultMiningRow> rows) {
        DefaultMiningRow aggregatedRow = aggregatedRows.get(columnValues);
        if (aggregatedRow == null) {
            aggregatedRows.put(columnValues, aggregatedRow = computeAggregatedRow(columnValues, rows));
        }

        return aggregatedRow;
    }
    void rowInserted(int index, int count) {
        persistentRows.itemInserted(index, count);
    }

    void rowRemoved(int index, int count) {
        persistentColumns.itemRemoved(index, count);
    }
    
    void rowUpdated(int index, int count) {
        if (treeListeners.isEmpty()) {
            return;
        }

        for (int rowIndex = index, m = index + count; rowIndex < m; rowIndex++) {
            DefaultMiningRow row = persistentRows.getItem(rowIndex, false);
            if (row == null) {
                continue; // Row instance was not created yet : ignore
            }
            DefaultMiningNode node = nodeByRow.get(row);
            if (node == null) {
                continue;
            }

            TreeModelEvent e = new TreeModelEvent(AbstractMiningModel.this, node.getPath());
            for (TreeModelListener l : treeListeners) {
                l.treeNodesChanged(e);
            }
        }
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        model.setValueAt(aValue, rowIndex, getUnderlyingColumnIndex(columnIndex));
    }
    final DefaultMiningNode root = new DefaultMiningNode(null, new DefaultMiningColumnValues(), null) {

        public String toString() {
            return "Root";
        }
    };

    @Override
    public Object getChild(Object parent, int index) {

        if (parent instanceof DefaultMiningNode) {
            DefaultMiningNode node = (DefaultMiningNode) parent;
            int groupIndex = node.columnValues.columnValues.length;
            if (groupIndex < groupColumns.size()) {
                return getNodeChildren(node).get(index);
            } else {
                return getNodeFilteredRows(node).get(index);
            }
        }
        return null;
    }

    private List<DefaultMiningRow> getNodeFilteredRows(DefaultMiningNode node) {
        List<DefaultMiningRow> ret = nodesFilteredRows.get(node);
        if (ret == null) {
            if (node == root) {
                int rowCount = model.getRowCount();
                ret = new ArrayList<DefaultMiningRow>(rowCount);
                for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
                    ret.add(getRow(rowIndex));
                }
            } else {
                DefaultColumnValue cv = node.getColumnValues().getLast();
                List<DefaultMiningRow> parentRows = getNodeFilteredRows(node.parent);
                ret = new ArrayList<DefaultMiningRow>(parentRows.size());
                for (DefaultMiningRow row : parentRows) {
                    if (cv.accept(row)) {
                        ret.add(row);
                    }
                }
            }
            nodesFilteredRows.put(node, ret);
        }
        return ret;
    }

    List<Object> getValues(List<DefaultMiningRow> rows, DefaultMiningColumn column) {
        Set<Object> values = new HashSet<Object>();
        boolean hasNull = false;
        for (DefaultMiningRow row : rows) {
            Object value = model.getValueAt(row.getUnderlyingIndex(), column.getUnderlyingIndex());
            if (value == null) {
                hasNull = true;
            } else {
                values.add(value);
            }
        }
        List<Object> ret = new ArrayList<Object>(values);
        if (hasNull) {
            ret.add(null);
        }
        return ret;
    }

    private List<DefaultMiningNode> getNodeChildren(DefaultMiningNode node) {
        List<DefaultMiningNode> nodeChildren = nodesChildren.get(node);
        if (nodeChildren == null && !groupColumns.isEmpty()) {
            List<DefaultMiningRow> nodeFilteredRows = getNodeFilteredRows(node);
            int columnGroupIndex = node.columnValues.columnValues.length;
            assert columnGroupIndex < groupColumns.size();

            DefaultMiningColumn column = groupColumns.get(columnGroupIndex);
            List<Object> values = getValues(nodeFilteredRows, column);
            nodeChildren = new ArrayList<DefaultMiningNode>(values.size());
            for (Object value : values) {
                DefaultMiningColumnValues columnValues = node.columnValues.derive(column, value);
                DefaultMiningNode childNode = new DefaultMiningNode(node, columnValues, getAggregatedRow(columnValues, nodeFilteredRows));
                nodeChildren.add(childNode);

                List<DefaultMiningNode> nodesForColumn = internalNodesByColumn.get(column);
                if (nodesForColumn == null) {
                    internalNodesByColumn.put(column, nodesForColumn = new ArrayList<DefaultMiningNode>());
                }
                nodesForColumn.add(childNode);
            }
            Collections.sort(nodeChildren, new Comparator<DefaultMiningNode>() {

                @Override
                public int compare(DefaultMiningNode o1, DefaultMiningNode o2) {
                    Object v1 = o1.getColumnValues().getLast().value;
                    Object v2 = o2.getColumnValues().getLast().value;
                    if (v1 instanceof Comparable)
                        return ((Comparable)v1).compareTo(v2);
                    return o1.toString().compareTo(o2.toString());
                }
            });
            nodesChildren.put(node, nodeChildren);
        }
        return nodeChildren;
    }

    @Override
    public int getChildCount(Object parent) {
        if (parent instanceof DefaultMiningRow) {
            return 0;
        }
        DefaultMiningNode node = (DefaultMiningNode) parent;
        int columnGroupIndex = node.columnValues.columnValues.length;
        if (columnGroupIndex < groupColumns.size()) {
            return getNodeChildren((DefaultMiningNode) parent).size();
        } else {
            return getNodeFilteredRows(node).size();
        }
    }

    @Override
    public int getIndexOfChild(Object parent, Object child) {
        if (parent instanceof DefaultMiningRow) {
            return -1;
        }
        List<DefaultMiningNode> nodeChildren = getNodeChildren((DefaultMiningNode) parent);
        return nodeChildren == null ? -1 : nodeChildren.indexOf(child);
    }

    @Override
    public DefaultMiningNode getRoot() {
        return root;
    }

    @Override
    public boolean isLeaf(Object node) {
        return node instanceof DefaultMiningRow;
    }

    @Override
    public void valueForPathChanged(TreePath path, Object newValue) {
        // TODO Auto-generated method stub
    }
}
